package com.zhouyx.redisactive.lock;

import org.springframework.beans.factory.annotation.Autowired;

import com.zhouyx.redisactive.utils.JedisUtils;

import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;

/**
 * @ClassName SimpleRedisLock
 * @Description Redis分布式锁
 *
 * 方案：通过setnx()方法尝试加锁，如果当前锁不存在，返回加锁成功。
 * 如果锁已经存在则获取锁的过期时间，和当前时间比较，如果锁已经过期，则设置新的过期时间，返回加锁成功。
 * 新的acquire方法，通过watch、redis事务，保证只有一个客户端能执行getset，
 * 并记录了锁超时时间，解决了问题一的麻烦。
 * 对于锁超时导致的两个客户端同时访问资源，
 * 要么靠业务代码保证锁超时时间内可以完成处理；
 * 要么在release时检查是否超时，如果超时回滚所有操作，但对不能回滚的，例如++操作就比较麻烦，或者放弃死锁容错功能。
 * Redis分布式锁的获取锁的问题就到这里了，具体怎么使用还要看实际业务场景
 *
 * 这严格来说是一种错误示例而且实现也比较复杂。那么这段代码问题在哪里？
 * 1. 由于是客户端自己生成过期时间，所以需要强制要求分布式下每个客户端的时间必须同步。
 * 2. 当锁过期的时候，如果多个客户端同时执行jedis.getSet()方法，那么虽然最终只有一个客户端可以加锁，
 * 但是这个客户端的锁的过期时间可能被其他客户端覆盖。
 * 3.锁不具备拥有者标识，即任何客户端都可以解锁
 *
 * @Author za-zhouyunxing
 * @Date 2019/4/16 16:02
 * @Version 1.0
 */
public class SimpleRedisLock {
    @Autowired
    private JedisUtils jedis;

    public static long hold_time = 3000;

    public static ThreadLocal<String> expireHolder = new ThreadLocal<>();

    public  void acquire(String lock){
        //1.先尝试用setnx命令获取锁,key为参数lock,值为当前时间+要持有锁的时间hold_time
        while(jedis.setnx(lock, String.valueOf(System.currentTimeMillis() + hold_time))){
            //2.如果获取失败,先watch lock key
            jedis.watch(lock);
            //3.获取当前超时时间
            String expireTime = jedis.get(lock);
            if(expireTime != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                //4.如果超时时间小于当前时间,开事务准备更新lock值
                Transaction transaction = jedis.multi();
                Response<String> response = transaction.getSet(lock, String.valueOf(System.currentTimeMillis() + hold_time));
                //5.步骤2设置了watch,如果lock的值被其他线程修改,不会执行事务中的命令
                if(transaction.exec() != null){
                    String oldExpire = response.get();
                    if(oldExpire != null && Long.parseLong(expireTime) < System.currentTimeMillis()){
                        //6.如果setget命令返回的值依然是过期时间,认为获取锁成功(加了watch之后,这里返回的应该一直是超时时间)
                        break;
                    }
                }
            }else{
                //如果key未超时,解除watch
                jedis.unwatch();
            }
        }
        //设置客户端超时时间
        expireHolder.set(jedis.get(lock));
    }

    public  void release(String lock){
        //比较客户端超时时间与lock值,判断是否还由自己持有锁
        if(jedis.get(lock).equals(expireHolder.get())){
            jedis.del(lock);
        }
    }
}
